﻿using System.Collections;
using System.Collections.Generic;
using System;

namespace Utility.FSM
{
	public enum AgentLogFlag
	{
		All = int.MaxValue,
		None = 0,

		State_Enter = 1 << 0,
		State_Exit = 1 << 1,
		State_Arrive = 1 << 2,
		State = State_Enter | State_Exit | State_Arrive,

		Event_Handled = 1 << 3,
		Event_Unhandled = 1 << 4,

		Event = Event_Handled | Event_Unhandled,
	}

	public class Agent
	{
		List<State> stateList = new List<State> ();
		bool inTransition = false;
		bool processSignal = true;

		public string Name { get; private set; }
		public FSM FSM { get; private set; }
		public int Depth { get; private set; } = -1;
		public AgentLogFlag LogFlag { get; set; } = AgentLogFlag.None;

		IAgentScript _script;
		public IAgentScript Script
		{
			get
			{
				return _script;
			}
			set
			{
				if (_script != value)
				{
					_script = value;
					_script.OnInit (this);
				}
			}
		}

		public TScript GetScript<TScript> ()
			where TScript : IAgentScript
		{
			return (TScript)_script;
		}

		public State State
		{
			get
			{
				return Depth < 0 ? null : stateList[Depth];
			}
		}

		//constructor
		public Agent (FSM fsm, string name, IAgentScript script = null)
		{
			FSM = fsm;
			Name = name;
			Script = script;
		}

		public void Dispose ()
		{
			if (_script != null)
			{
				_script.OnDispose (this);
			}
		}

		public bool IsIn (State state)
		{
			return stateList.Contains (state);
		}

		public bool CanLog (AgentLogFlag flag)
		{
			return (flag & LogFlag) > 0;
		}

		public void Log (string msg, UnityEngine.LogType logType = UnityEngine.LogType.Log)
		{
			msg = string.Format ("[{0}]{1}", Name, msg);
			switch (logType)
			{
				case UnityEngine.LogType.Error:
				case UnityEngine.LogType.Exception:
				case UnityEngine.LogType.Assert:
					UnityEngine.Debug.LogError (msg);
					break;
				case UnityEngine.LogType.Warning:
					UnityEngine.Debug.LogWarning (msg);
					break;
				default:
					UnityEngine.Debug.Log (msg);
					break;
			}
		}

		#region invoke event
		public bool SendSignal (string signal, object context = null)
		{
			if (Depth < 0)
			{
				Log ("Agent is in Null State", UnityEngine.LogType.Warning);
				return false;
			}

			bool handled = false;
			//process captured signals: from root to leaf
			for (int i = 0; i < stateList.Count; i++)
			{
				var state = stateList[i];
				if (state.Script != null)
				{
					handled |= state.Script.OnCaptureSignal (signal, state, this, context);
				}
			}
			//process signals: from leaf to root
			processSignal = true;
			for (int i = stateList.Count - 1; i >= 0; i--)
			{
				if (!processSignal)
				{
					return handled;
				}

				var state = stateList[i];
				if (state.Script != null)
				{
					handled |= state.Script.OnSignal (signal, state, this, context);
				}
			}

			if (handled)
			{
				if (CanLog (AgentLogFlag.Event_Handled))
				{
					Log ("Command handled: " + signal);
				}
			}
			else
			{
				if (CanLog (AgentLogFlag.Event_Unhandled))
				{
					Log ("Command unhandled: " + signal);
				}
			}
			return handled;
		}

		public void Update (object context = null)
		{
			for (int i = 0; i < stateList.Count; i++)
			{
				var state = stateList[i];
				state.Script?.OnCaptureUpdateState (state, this, context);
			}
			processSignal = true;
			for (int i = stateList.Count - 1; i >= 0; i--)
			{
				if (!processSignal)
				{
					break;
				}

				var state = stateList[i];
				state.Script?.OnUpdateState (state, this, context);
			}
			Script?.OnUpdate (this, State, context);
		}

		/// <summary>
		/// stop passing the signal to the upper state 
		/// </summary>
		public void ConsumeSignal ()
		{
			processSignal = false;
		}
		#endregion
		#region state transition
		#region helper
		//buffers
		static List<State> _path = new List<State> ();
		static List<State> _enterList = new List<State> ();

		//note that enterPath is in reverse order
		//all result is added to the list, additively
		void _FindPath (State from, State to, List<State> list)
		{
			if (from == to)
			{
				return;
			}

			var depth_from = _GetDepth (from);
			var depth_to = _GetDepth (to);

			//reach up from side
			while (depth_from > depth_to)
			{
				list.Add (from);
				from = from.UpperState;
				depth_from--;
			}

			//reach up to side
			while (depth_from < depth_to)
			{
				_enterList.Add (to);
				to = to.UpperState;
				depth_to--;
			}

			//reach up both until meet
			while (from != to)
			{
				list.Add (from);
				from = from.UpperState;
				_enterList.Add (to);
				to = to.UpperState;
			}

			//add pivot
			list.Add (from);
			//add buffered states in reverse order
			for (int i = _enterList.Count - 1; i >= 0; i--)
			{
				list.Add (_enterList[i]);
			}
			_enterList.Clear ();
		}

		//first enter, then events
		void _EnterState (State state, object context)
		{
			Depth++;
			stateList.Add (state);

			if (CanLog (AgentLogFlag.State_Enter))
			{
				Log ("enter state: " + state.Fullname);
			}
			//call event
			state.Script?.OnEnterState (state, this, context);
			Script?.OnEnterState (this, state, context);
		}

		//first event, then exit
		void _ExitState (State state, object context)
		{
			if (CanLog (AgentLogFlag.State_Exit))
			{
				Log ("exit state: " + state.Fullname);
			}
			//call event
			state.Script?.OnExitState (state, this, context);
			Script?.OnExitState (this, state, context);

			//actually exit
			Depth--;
			stateList.RemoveAt (stateList.Count - 1);
		}

		void _ArriveState (State state, object context)
		{
			if (CanLog (AgentLogFlag.State_Arrive))
			{
				Log ("arrive state: " + (state == null ? "null" : state.Fullname));
			}
			//call event
			state?.Script?.OnArriveState (state, this, context);
			Script?.OnArriveState (this, state, context);
		}

		int _GetDepth (State state)
		{
			return state == null ? -1 : state.Depth;
		}

		//if a==b, or a is sub of b, or b is sub of a, return true
		bool _IsConnected (State a, State b)
		{
			return a == b || a?.UpperState == b || b?.UpperState == a;
		}

		//trust that path is reasonable
		//the path should include its start and end state, and each pivot state
		void _MovePath (List<State> path, object context)
		{
			inTransition = true;
			State curr, next = null;
			bool moved = false;
			for (int i = 0; i < path.Count - 1; i++)
			{
				curr = path[i];
				next = path[i + 1];
				int d_depth = _GetDepth (curr) - _GetDepth (next);
				if (_IsConnected (curr, next))
				{
					switch (d_depth)
					{
						case -1://move down
							_EnterState (next, context);
							moved = true;
							break;
						case 1://move up
							_ExitState (curr, context);
							moved = true;
							break;
						case 0:
							//do nothing
							break;
					}
				}
				else
				{
					throw new Exception ("The path is not connected");
				}
			}
			inTransition = false;
			if (moved)
			{
				_ArriveState (next, context);
			}
		}

		bool _IsInValid (State state)
		{
			return state != null && !FSM.Contains (state);
		}
		#endregion

		/// <summary>
		/// Move to target state
		/// </summary>
		/// <param name="targetState"></param>
		/// <param name="context"></param>
		public void MoveTo (State targetState, object context = null)
		{
			if (inTransition)
			{
				return;
			}

			if (_IsInValid (targetState))
			{
				throw new Exception ("state is not part of the fsm: " + targetState.Fullname);
			}

			_path.Clear ();
			_FindPath (State, targetState, _path);
			_MovePath (_path, context);
		}

		public void MoveTo (State target_0, State target_1, object context = null)
		{
			if (inTransition)
			{
				return;
			}

			if (_IsInValid (target_0))
			{
				throw new Exception ("state is not part of the fsm: " + target_0.Fullname);
			}
			if (_IsInValid (target_1))
			{
				throw new Exception ("state is not part of the fsm: " + target_1.Fullname);
			}

			_path.Clear ();
			_FindPath (State, target_0, _path);
			_FindPath (target_0, target_1, _path);
			_MovePath (_path, context);
		}

		/// <summary>
		/// Enter target state, which must be an descendant state
		/// </summary>
		/// <param name="state"></param>
		public void EnterState (State state, object context = null)
		{
			if (inTransition)
			{
				return;
			}

			if (State == null || State.IsAncestorOf (state))
			{
				MoveTo (state, context);
			}
		}



		/// <summary>
		/// Exit current state
		/// </summary>
		/// <param name="context"></param>
		public void ExitState (object context = null)
		{
			ExitState (State, context);
		}

		/// <summary>
		/// Exit the state, the state must be one of the upper state
		/// </summary>
		/// <param name="state"></param>
		public void ExitState (State state, object context = null)
		{
			if (inTransition)
			{
				return;
			}

			if (!IsIn (state))
			{
				throw new Exception ("invalid exit state target: " + state.Fullname);
			}

			MoveTo (state.UpperState, context);
		}

		public void Reenter (State state, object context)
		{
			if (inTransition)
			{
				return;
			}

			if (!IsIn (state))
			{
				return;
			}

			_path.Clear ();
			_FindPath (State, state, _path);
			_path.Add (state.UpperState);
			_path.Add (state);

			_MovePath (_path, context);
		}

		/// <summary>
		/// From null state enter the Root state
		/// </summary>
		/// <param name="context"></param>
		public void EnterFSM (object context = null)
		{
			if (inTransition)
			{
				return;
			}
			if (State == null)
			{
				MoveTo (FSM.Root, context);
			}
		}

		/// <summary>
		/// Exit from current state up to the Root
		/// Until state is null
		/// </summary>
		/// <param name="context"></param>
		public void ExitFSM (object context = null)
		{
			if (inTransition)
			{
				return;
			}
			MoveTo ((State)null, context);
		}
		#endregion
		#region transition by name
		public void MoveTo (string targetStateName, object context = null)
		{
			var target = FSM.GetState (targetStateName, true);
			MoveTo (target, context);
		}
		public void MoveToSibling (string targetStateName, object context = null)
		{
			MoveToSibling (targetStateName, false, context);
		}

		public void MoveToSibling (string targetStateName, bool reenterSelf, object context = null)
		{
			if (State == null)
			{
				throw new Exception ("Cannot do this while in null state");
			}
			var target = State.GetSiblingState (targetStateName, true);

			if (target == State)
			{
				if (reenterSelf)
				{
					ExitState (context);
				}
				else
				{
					return;
				}
			}
			MoveTo (target, context);
		}
		public void EnterState (string stateName, object context = null)
		{
			State target;
			if (State == null)
			{
				target = FSM.GetState (stateName, true);
			}
			else
			{
				target = State.GetSubstate (stateName, true);
			}
			EnterState (target, context);
		}
		public void ExitState (string stateName, object context = null)
		{
			var target = FSM.GetState (stateName, true);
			ExitState (target, context);
		}
		#endregion

	}



}